﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Text;
using System.IO;
using System.Security.Cryptography;
using System.Data;
using System.Threading;

namespace TetrisService
{
    /// <summary>
    /// This class is responsible for managing the data base of the application. In addition, it is responsible
    /// for encrypting the contents of the data base.
    /// </summary>
    public class TetrisDataBase
    {
        private readonly int BUFFER_SIZE = 4096; // 4KB
        private TetrisDataSet rawDataSet;
        private string fileName;
        private string password;
        private byte[] key;
        private byte[] initializationVector;

        /// <summary>
        /// Initializes a new instance of the <see cref="TetrisDataBase"/> class.
        /// </summary>
        /// <param name="fileName">Name of the file.</param>
        /// <param name="password">The password.</param>
        public TetrisDataBase(string fileName, string password)
        {
            this.fileName = fileName;
            this.password = password;
            rawDataSet = new TetrisDataSet();
            configureVariables();
            loadEncrypted();
        }

        /// <summary>
        /// Converts a string in an array of bytes.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        private byte[] stringToByteArrayUnicode(string password)
        {
            Encoding unicode = Encoding.Unicode;
            return unicode.GetBytes(password);
        }

        /// <summary>
        /// Gets the cipher algorithm.
        /// </summary>
        /// <returns></returns>
        private SymmetricAlgorithm getCipherAlgorithm()
        {
            return new RijndaelManaged();
        }

        /// <summary>
        /// Configures the key and the initialization vector.
        /// </summary>
        private void configureVariables()
        {
            key = stringToByteArrayUnicode(password);
            initializationVector = stringToByteArrayUnicode(password);
        }

        /// <summary>
        /// Loads the encrypted file which holds the scores.
        /// </summary>
        private void loadEncrypted()
        {
            if(!File.Exists(fileName)) {
                rawDataSet.user.PrimaryKey = new DataColumn[] { rawDataSet.user.usernameColumn };
                return;
            }

            FileInfo file = new FileInfo(fileName);
            byte[] encryptedDataBuffer = new byte[file.Length];

            FileStream fileStream = new FileStream(fileName, FileMode.Open);

            SymmetricAlgorithm cipherAlgorithm = getCipherAlgorithm();
            cipherAlgorithm.Padding = PaddingMode.Zeros;

            fileStream.Position = 0;
            fileStream.Read(encryptedDataBuffer, 0, encryptedDataBuffer.Length);

            MemoryStream encryptedMemoryStream = new MemoryStream(encryptedDataBuffer);
            CryptoStream decryptStream = new CryptoStream(encryptedMemoryStream, 
                cipherAlgorithm.CreateDecryptor(key, initializationVector), CryptoStreamMode.Read);

            rawDataSet.ReadXml(decryptStream, XmlReadMode.Auto);
            encryptedMemoryStream.Flush();
            encryptedMemoryStream.Close();
            fileStream.Close();
        }

        /// <summary>
        /// Saves the encrypted file which holds the scores.
        /// </summary>
        private void saveEncrypted()
        {
            FileStream fileStream = new FileStream(fileName, FileMode.Create);
            MemoryStream unencryptedMemoryStream = new MemoryStream();
            rawDataSet.WriteXml(unencryptedMemoryStream);
            unencryptedMemoryStream.Position = 0;

            SymmetricAlgorithm cipherAlgorithm = getCipherAlgorithm();
            cipherAlgorithm.Padding = PaddingMode.Zeros;
            CryptoStream ecryptStream = new CryptoStream(fileStream, 
                cipherAlgorithm.CreateEncryptor(key, initializationVector), CryptoStreamMode.Write);

            encryptContent(unencryptedMemoryStream, ecryptStream);
            ecryptStream.FlushFinalBlock();
            ecryptStream.Close();   
            fileStream.Close();
            unencryptedMemoryStream.Close();
        }

        /// <summary>
        /// Encrypts the provided content.
        /// </summary>
        /// <param name="unencryptedMemoryStream">The unencrypted memory stream.</param>
        /// <param name="ecryptStream">The ecrypt stream.</param>
        private void encryptContent(MemoryStream unencryptedMemoryStream, CryptoStream ecryptStream)
        {
            byte[] buffer = new byte[BUFFER_SIZE];
            int position = 0;
            int sizeOfCurrentChunk;

            do
            {
                sizeOfCurrentChunk = unencryptedMemoryStream.Read(buffer, 0, buffer.Length);
                ecryptStream.Write(buffer, 0, sizeOfCurrentChunk);
                position += sizeOfCurrentChunk;
            } while (position < unencryptedMemoryStream.Length && streamHasContentToRead(position, sizeOfCurrentChunk));
        }

        /// <summary>
        /// Check if there is still content to be read.
        /// </summary>
        /// <param name="position">The position.</param>
        /// <param name="sizeOfCurrentChunk">The size of current chunk.</param>
        /// <returns></returns>
        private bool streamHasContentToRead(int position, int sizeOfCurrentChunk)
        {
            return sizeOfCurrentChunk == 0 && position == 0;
        }

        /// <summary>
        /// Adds the provided user to the data base.
        /// </summary>
        /// <param name="userName">Name of the user.</param>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        public bool addUser(string userName, string password)
        {
            Monitor.Enter(this);
            try
            {
                if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password) ||
                    getUser(userName) != null)
                {
                    return false;
                }
                rawDataSet.user.AdduserRow(userName, password, new int[6]);
                saveEncrypted();
                return true;
            }
            finally
            {
                Monitor.Exit(this);
            }
        }

        /// <summary>
        /// Updates the scores of the user.
        /// </summary>
        /// <param name="user">The user.</param>
        /// <returns></returns>
        public bool updateUser(User user)
        {
            Monitor.Enter(this);
            try
            {
                int index = getIndex(user.getUserName());
                if (index < 0)
                {
                    return false;
                }
                rawDataSet.user[index]["scores"] = user.getScores();
                saveEncrypted();
                return true;
            }
            finally
            {
                Monitor.Exit(this);
            }
        }

        /// <summary>
        /// Gets the user by its name.
        /// </summary>
        /// <param name="userName">Name of the user.</param>
        /// <returns></returns>
        public User getUser(string userName)
        {
            Monitor.Enter(this);
            try
            {
                TetrisDataSet.userRow row = findUser(userName);
                if (row != null)
                {
                    return new User(row.username, row.password, row.scores);
                }
                return null;
            }
            finally
            {
                Monitor.Exit(this);
            }
        }

        /// <summary>
        /// Gets the fives the best scores.
        /// </summary>
        /// <returns></returns>
        public string[] fiveBestScores()
        {
            List<string> userNames = new List<string>(5);
            List<int> score = new List<int>(5);

            for (int i = 0; i < rawDataSet.user.Count; i++)
            {
                User user = getUserByIndex(i);
                for (int j = 0; j < user.getValidScores().Length; j++)
                {
                    int index = isABestScore(score, user.getValidScores()[j]);
                    score.Insert(index, user.getValidScores()[j]);
                    userNames.Insert(index, user.getUserName());
                }
            }

            string[] result = new string[5];
            for (int k = 0; k < result.Length && k < score.Count; k++)
            {
                if (score.ElementAt<int>(k) > 0)
                {
                    result[k] = userNames.ElementAt<string>(k) + " " + score.ElementAt<int>(k);
                }
            }
            return result;
        }

        /// <summary>
        /// Check if the new score should belong to the five best scores.
        /// </summary>
        /// <param name="scores">The scores.</param>
        /// <param name="value">The value.</param>
        /// <returns></returns>
        private int isABestScore(List<int> scores, int value)
        {
            int index = 0;

            for (int i = 0; i < scores.Count && value < scores.ElementAt<int>(index); i++)
            {
                index++;
            }
            return index;
        }

        /// <summary>
        /// Gets the index of the user by its the index.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <returns></returns>
        private User getUserByIndex(int index)
        {
            if (index >= 0 && index < rawDataSet.user.Count)
            {
                TetrisDataSet.userRow row = rawDataSet.user.ElementAt(index); ;
                return new User(row.username, row.password, row.scores);
            }
            return null;
        }


        /// <summary>
        /// Gets the index of the user based on its user name.
        /// </summary>
        /// <param name="userName">Name of the user.</param>
        /// <returns></returns>
        private int getIndex(string userName)
        {
            for (int i = 0; i < rawDataSet.user.Count; i++)
            {
                if (rawDataSet.user.ElementAt(i).username == userName)
                {
                    return i;
                }
            }
            return -1;
        }

        /// <summary>
        /// Finds the user row by its user name.
        /// </summary>
        /// <param name="userName">Name of the user.</param>
        /// <returns></returns>
        private TetrisDataSet.userRow findUser(string userName)
        {
            for (int i = 0; i < rawDataSet.user.Count; i++)
            {
                if (rawDataSet.user.ElementAt(i).username == userName)
                {
                    return rawDataSet.user.ElementAt(i);
                }
            }
            return null;
        }
    }
}